Tanzania es un país en desarrollo que se encuentra en medio de una crisis de agua. El país lucha por proporcionar agua potable a su población de más de 57 millones de personas a pesar de que tiene muchos puntos de agua establecidos. El gobierno de Tanzania, a través del Ministerio del Agua, tiene como objetivo resolver la crisis del agua en el país manteniendo y reparando las bombas de agua a tiempo. Para que sus ingenieros logren los objetivos más rápidamente, deben saber de antemano qué bombas de agua tienen probabilidades de fallar y comprender las causas de las fallas.
Por lo tanto, el proyecto tiene como objetivo desarrollar un modelo utilizando datos recopilados por Taarifa y el Ministerio de Agua de Tanzania para predecir el estado de funcionalidad de las bombas de agua en Tanzania, así como proporcionar información importante sobre los principales factores que contribuyen a las fallas de las bombas de agua en Tanzania. El modelo creado permitirá al Ministerio del Agua de Tanzania mejorar las operaciones de mantenimiento de sus bombas de agua.
Los datos utilizados en este proyecto provienen del concurso Pump it Up: Data Mining the Water Table organizado por DrivenData, originalmente obtenido por Taarifa y el Ministerio de Agua de Tanzania.
Limpieza de datos: donde se inspeccionan todas las variables y se toman los pasos necesarios para eliminar valores faltantes, corregir errores, eliminar características redundantes y aplicar las transformaciones necesarias.
Ingeniería de funciones: donde se crean algunas funciones nuevas a partir de la combinación de otras funciones existentes.
Exploración: donde se analiza la relación entre algunas variables interesantes status_group.
Modelos de referencia: donde se ajustan los siguientes modelos a los datos para obtener una puntuación de referencia: Random Forest, XGBoost,Baggin y VojtingClasifier.
Ajuste de hiperparámetros: donde los parámetros de Random Rorest y XGBoost se ajustan mediante búsquedas aleatorias iteradas. Mejor puntuación general (0,8207) obtenida mediante bosque aleatorio después del ajuste de parámetros.
Se evaluaron diferentes modelos y se eligió el modelo de mejor rendimiento como modelo final siendo éste VotingClassifier, que comibina predicciones de variosmodelos(en este caso de los anteriores).
Experimentando con diferentes modelos utilizados, en este trabajo, sólo figuran los modelos con mejores puntuaciones.
#EDA
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from ydata_profiling import ProfileReport
import scipy.stats as stats
from scipy.stats import chi2_contingency, fisher_exact
#Depuración
import missingno as msno
import sklearn.impute as skl_imp
from sklearn.experimental import enable_iterative_imputer
import feature_engine.imputation as fe_imp
#Codificación
import category_encoders as ce
from sklearn import preprocessing
#feature engineering
from optbinning import MulticlassOptimalBinning
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler
#data modelling
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from xgboost import XGBClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import HalvingGridSearchCV
from sklearn.ensemble import BaggingClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.ensemble import RandomForestClassifier,AdaBoostClassifier, GradientBoostingClassifier
#evaluation
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, roc_curve, classification_report
#setup
import warnings
warnings.simplefilter("ignore")
pd.set_option('display.max_columns', None)
def cramers_v(var1, varObj):
"""Función que calcula v de cramer en relación a la variable objetivo"""
if not var1.dtypes == 'object':
#bins = min(5,var1.value_counts().count())
var1 = pd.cut(var1, bins = 5)
if not varObj.dtypes == 'object': #np.issubdtype(varObj, np.number):
#bins = min(5,varObj.value_counts().count())
varObj = pd.cut(varObj, bins = 5)
data = pd.crosstab(var1, varObj).values
vCramer = stats.contingency.association(data, method = 'cramer')
return vCramer
def winsorize_with_pandas(s, limits):
"""
Función para colapsar los valores extremos.
s : pd.Series
Series to winsorize
limits : tuple of float
Tuple of the percentages to cut on each side of the array,
with respect to the number of unmasked data, as floats between 0. and 1
"""
return s.clip(lower=s.quantile(limits[0], interpolation='lower'), #clip colapsa en un límite
upper=s.quantile(1-limits[1], interpolation='higher'))
def saca_metricas(y_true, y_pred, y_proba):
print('matriz de confusión')
print(confusion_matrix(y_true, y_pred))
print('accuracy')
print(accuracy_score(y_true, y_pred))
print('precision')
print(precision_score(y_true, y_pred, average='weighted'))
print('recall')
print(recall_score(y_true, y_pred, average='weighted'))
print('f1')
print(f1_score(y_true, y_pred, average='weighted'))
print('ROC-AUC OvR: ')
print(roc_auc_score(y_true, y_proba, multi_class='ovr'))
print('ROC-AUC OvO: ')
print(roc_auc_score(y_true, y_proba, multi_class='ovo'))
train = pd.read_csv('train_set.csv')
labels = pd.read_csv('train_labels.csv')
test = pd.read_csv('test_set.csv')
labels
| id | status_group | |
|---|---|---|
| 0 | 69572 | functional |
| 1 | 8776 | functional |
| 2 | 34310 | functional |
| 3 | 67743 | non functional |
| 4 | 19728 | functional |
| ... | ... | ... |
| 59395 | 60739 | functional |
| 59396 | 27263 | functional |
| 59397 | 37057 | functional |
| 59398 | 31282 | functional |
| 59399 | 26348 | functional |
59400 rows × 2 columns
print(labels.shape)
print(train.shape)
print(test.shape)
(59400, 2) (59400, 40) (14850, 40)
#verificamos la coincidencia de los ids para saber si podemos concatenar sin problemas
print('Coincidencia ids train/test:\n', train['id'].isin(test['id']).value_counts())
Coincidencia ids train/test: id False 59400 Name: count, dtype: int64
print('Comprobamos si las columnas son las mismas: \n ', test.columns.isin(train.columns))
Comprobamos si las columnas son las mismas: [ True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True]
#columna id como índice
train.set_index('id', inplace=True)
labels.set_index('id', inplace=True)
test.set_index('id', inplace=True)
#guardamos los indices antes de concatenar para poder separar cuando sea necesario
indices_test = test.index.to_list()
#concatenamos los datos para el EDA a la vez que unimos la variable objetivo
df = pd.concat([train.join(labels), test])
data= df.copy(deep=True)
data.info()
<class 'pandas.core.frame.DataFrame'> Index: 74250 entries, 69572 to 68707 Data columns (total 40 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 amount_tsh 74250 non-null float64 1 date_recorded 74250 non-null object 2 funder 69743 non-null object 3 gps_height 74250 non-null int64 4 installer 69718 non-null object 5 longitude 74250 non-null float64 6 latitude 74250 non-null float64 7 wpt_name 74248 non-null object 8 num_private 74250 non-null int64 9 basin 74250 non-null object 10 subvillage 73780 non-null object 11 region 74250 non-null object 12 region_code 74250 non-null int64 13 district_code 74250 non-null int64 14 lga 74250 non-null object 15 ward 74250 non-null object 16 population 74250 non-null int64 17 public_meeting 70095 non-null object 18 recorded_by 74250 non-null object 19 scheme_management 69403 non-null object 20 scheme_name 38198 non-null object 21 permit 70457 non-null object 22 construction_year 74250 non-null int64 23 extraction_type 74250 non-null object 24 extraction_type_group 74250 non-null object 25 extraction_type_class 74250 non-null object 26 management 74250 non-null object 27 management_group 74250 non-null object 28 payment 74250 non-null object 29 payment_type 74250 non-null object 30 water_quality 74250 non-null object 31 quality_group 74250 non-null object 32 quantity 74250 non-null object 33 quantity_group 74250 non-null object 34 source 74250 non-null object 35 source_type 74250 non-null object 36 source_class 74250 non-null object 37 waterpoint_type 74250 non-null object 38 waterpoint_type_group 74250 non-null object 39 status_group 59400 non-null object dtypes: float64(3), int64(6), object(31) memory usage: 23.2+ MB
data.nunique()
amount_tsh 102 date_recorded 369 funder 2139 gps_height 2456 installer 2410 longitude 71870 latitude 71869 wpt_name 45683 num_private 68 basin 9 subvillage 21425 region 21 region_code 27 district_code 20 lga 125 ward 2098 population 1128 public_meeting 2 recorded_by 1 scheme_management 11 scheme_name 2867 permit 2 construction_year 55 extraction_type 18 extraction_type_group 13 extraction_type_class 7 management 12 management_group 5 payment 7 payment_type 7 water_quality 8 quality_group 6 quantity 5 quantity_group 5 source 10 source_type 7 source_class 3 waterpoint_type 7 waterpoint_type_group 6 status_group 3 dtype: int64
#porcentaje de nulos declarados
data.isna().sum() / len(data) * 100
amount_tsh 0.000000 date_recorded 0.000000 funder 6.070034 gps_height 0.000000 installer 6.103704 longitude 0.000000 latitude 0.000000 wpt_name 0.002694 num_private 0.000000 basin 0.000000 subvillage 0.632997 region 0.000000 region_code 0.000000 district_code 0.000000 lga 0.000000 ward 0.000000 population 0.000000 public_meeting 5.595960 recorded_by 0.000000 scheme_management 6.527946 scheme_name 48.554882 permit 5.108418 construction_year 0.000000 extraction_type 0.000000 extraction_type_group 0.000000 extraction_type_class 0.000000 management 0.000000 management_group 0.000000 payment 0.000000 payment_type 0.000000 water_quality 0.000000 quality_group 0.000000 quantity 0.000000 quantity_group 0.000000 source 0.000000 source_type 0.000000 source_class 0.000000 waterpoint_type 0.000000 waterpoint_type_group 0.000000 status_group 20.000000 dtype: float64
data.select_dtypes(include=np.number).describe().round(2)
| amount_tsh | gps_height | longitude | latitude | num_private | region_code | district_code | population | construction_year | |
|---|---|---|---|---|---|---|---|---|---|
| count | 74250.00 | 74250.00 | 74250.00 | 74250.00 | 74250.00 | 74250.00 | 74250.00 | 74250.00 | 74250.00 |
| mean | 318.69 | 665.67 | 34.07 | -5.70 | 0.46 | 15.27 | 5.63 | 180.75 | 1298.46 |
| std | 2906.76 | 692.76 | 6.57 | 2.94 | 11.54 | 17.51 | 9.64 | 471.09 | 952.35 |
| min | 0.00 | -90.00 | 0.00 | -11.65 | 0.00 | 1.00 | 0.00 | 0.00 | 0.00 |
| 25% | 0.00 | 0.00 | 33.09 | -8.53 | 0.00 | 5.00 | 2.00 | 0.00 | 0.00 |
| 50% | 0.00 | 364.00 | 34.91 | -5.03 | 0.00 | 12.00 | 3.00 | 25.00 | 1986.00 |
| 75% | 20.00 | 1317.00 | 37.18 | -3.33 | 0.00 | 17.00 | 5.00 | 215.00 | 2004.00 |
| max | 350000.00 | 2777.00 | 40.35 | -0.00 | 1776.00 | 99.00 | 80.00 | 30500.00 | 2013.00 |
#histograma de las variables numéricas
plot = data.select_dtypes(include=np.number).hist(figsize=(10, 12))
#boxplot de las variables numéricas
# Establece el tamaño de la figura
plt.figure(figsize=(10, 6))
# Itera a través de las columnas numéricas y crea subplots
for i, column in enumerate(data.select_dtypes(include=np.number).columns):
plt.subplot(1, len(data.select_dtypes(include=np.number).columns), i + 1)
sns.boxplot(data[column], showfliers=False) # Oculta los valores atípicos
plt.title(column)
plt.tight_layout() # Ajusta el espaciado entre subplots
plt.show()
Para el análisis, hemos utilizado la librería ydata_profiling, pero como requiere de un tiempo de ejecución, la hemos dejado comentada para ser ejecutada a gusto propio. De todas formas, nuestras conclusiones están detalladas a continuación.
profile = ProfileReport(data, title="Profile Report Water Pumps")
profile
Summarize dataset: 0%| | 0/5 [00:00<?, ?it/s]
Generate report structure: 0%| | 0/1 [00:00<?, ?it/s]
Render HTML: 0%| | 0/1 [00:00<?, ?it/s]